﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Runtime.Serialization;

namespace Emico.Linq.Criterion
{
    [Serializable]
    public class Criteria<T> : ICriteria<T>
        where T : class
    {
        #region Variables

        public readonly static ParameterExpression Parameter = Expression.Parameter(typeof(T), "T");

        #endregion

        #region Properties

        public int FirstResult
        {
            get;
            set;
        }

        public int? MaxResults
        {
            get;
            set;
        }

        public IExpression<T> Filter
        {
            get;
            private set;
        }

        public IList<IOrderBy<T>> Orders
        {
            get;
            private set;
        }

        public IDictionary<string, object> Parameters
        {
            get;
            private set;
        }

        #endregion

        #region Constructors

        public Criteria()
            : this(new EmptyExpression<T>(), new IOrderBy<T>[0])
        {
        }
        public Criteria(IEnumerable<IOrderBy<T>> orders)
            : this(new EmptyExpression<T>(), orders)
        {
        }

        public Criteria(IExpression<T> filter)
            : this(filter, new IOrderBy<T>[0])
        {
        }

        public Criteria(IExpression<T> filter, IEnumerable<IOrderBy<T>> orders)
        {
            if (filter == null)
            {
                throw new ArgumentNullException("filter");
            }

            if (orders == null)
            {
                throw new ArgumentNullException("orders");
            }

            this.Filter = filter;
            this.Orders = new List<IOrderBy<T>>(orders);
            this.Parameters = new Dictionary<string, object>();
            this.FirstResult = 0;
        }

        protected Criteria(SerializationInfo info, StreamingContext context)
        {
            this.FirstResult = info.GetInt32("FirstResult");
            this.MaxResults = (int?)info.GetValue("MaxResults", typeof(int?));
            this.Filter = (IExpression<T>)info.GetValue("Filter", typeof(IExpression<T>));
            this.Orders = (IList<IOrderBy<T>>)info.GetValue("Orders", typeof(IList<IOrderBy<T>>));
            this.Parameters = (IDictionary<string, object>)info.GetValue("Parameters", typeof(IDictionary<string, object>));
        }

        #endregion

        #region Public method

        public IQueryable<T> AsQueryable(IEnumerable<T> source)
        {
            return AsQueryable(source.AsQueryable());
        }

        public IQueryable<T> AsQueryable(IQueryable<T> source)
        {
            IQueryable<T> q = source;

            q = ApplyFilter(q);
            q = ApplyOrder(q);

            if (this.FirstResult != 0)
            {
                q = q.Skip(this.FirstResult);
            }

            if (this.MaxResults.HasValue)
            {
                q = q.Take(this.MaxResults.Value);
            }

            return q;
        }

        #endregion

        #region Private method

        private IQueryable<T> ApplyFilter(IQueryable<T> source)
        {
            IQueryable<T> q = source;
            if (this.Filter != null)
            {
                Expression expression = this.Filter.ToLinqExpression(this, Parameter);
                q = source.Where(Expression.Lambda<Func<T, bool>>(expression, Parameter));                
            }

            return q;
        }

        private IQueryable<T> ApplyOrder(IQueryable<T> source)
        {
            Expression queryExpr = source.Expression;
            string methodAsc = "OrderBy";
            string methodDesc = "OrderByDescending";
            foreach (OrderBy<T> order in this.Orders)
            {
                Expression selector = order.ToLinqExpression(this, Parameter);

                queryExpr = Expression.Call(
                        typeof(Queryable),
                        order.SortDirection == ListSortDirection.Ascending ? methodAsc : methodDesc,
                        new Type[] { source.ElementType, selector.Type },
                        queryExpr,
                        Expression.Lambda(selector, Parameter)
                        );


                methodAsc = "ThenBy";
                methodDesc = "ThenByDescending";
            }

            return source.Provider.CreateQuery<T>(queryExpr);
        }

        #endregion

        #region ISerializable
        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("FirstResult", this.FirstResult);
            info.AddValue("MaxResults", this.MaxResults);
            info.AddValue("Filter", this.Filter);
            info.AddValue("Orders", this.Orders);
            info.AddValue("Parameters", this.Parameters);
        }

        #endregion
    }
}
